-
Notifications
You must be signed in to change notification settings - Fork 29.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
util: add private fields preview support #31817
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, this PR looks good 👍
I feel like it’s worth having this come with a benchmark – I’d guess this makes inspecting private properties rather expensive, and that’s probably okay but it might be nice to get a better idea of what the performance impact is.
src/inspector_js_api.cc
Outdated
@@ -87,18 +87,26 @@ class JSBindingsConnection : public AsyncWrap { | |||
JSBindingsConnection* connection_; | |||
}; | |||
|
|||
JSBindingsConnection(Environment* env, | |||
JSBindingsConnection(bool is_sync, | |||
Environment* env, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tiny nit, obviously not super important: Can you make is_sync
the last parameter instead of the first? The signatures for all BaseObject
subclasses take env
and wrap
as the first arguments, to that would make it a bit more consistent
lib/internal/util/inspect.js
Outdated
let previews; | ||
privateInspectionSession.post('Runtime.enable'); | ||
privateInspectionSession.post('Debugger.enable'); | ||
privateInspectionSession.once('Debugger.paused', (event) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we just have like a globalThis.__inspect_target
that we create and use and then delete, so we don't have to deal with the huge perf hit from forcing evaluation through the debug interpreter?
lib/internal/util/inspect.js
Outdated
privateInspectionSession.post('Runtime.enable'); | ||
privateInspectionSession.post('Debugger.enable'); | ||
privateInspectionSession.once('Debugger.paused', (event) => { | ||
const invokeFrame = event.params.callFrames[2]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned in #27110 (comment) these events only happen to be synchronous so that currently the listeners can be invoked before util.inspect returns. I guess we either need to mention the risk about that this can be broken in a future v8 update in the doc, or communicate with the V8 team to make this synchronicity part of the protocol (which seems difficult though).
cc @nodejs/v8
ce94b21
to
df6fb04
Compare
lib/internal/inspector.js
Outdated
// to store the result in this[resultSymbol]. | ||
debug(`dispatching message #${id}:`, message); | ||
this[connectionSymbol].dispatch(JSONStringify(message)); | ||
const result = this[resultSymbol]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that implicit synchronicity is still there - it's not guaranteed by V8 that the result would show up as this[resultSymbol]
right after the call to this[connectionSymbol].dispatch(JSONStringify(message))
, before post()
returns. It only happens to work right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The performance impact is frightening (-99.x%). We could consider adding an experimental option that we remove at some point to activate this for now.
Test wise we also have to check for already running inspector sessions. The user could already have a running session and the REPL itself also uses inspector sessions. I expect the inspection to fail in case another session is open? This should be clearly documented.
lib/internal/util/inspect.js
Outdated
let protoProps; | ||
if (ctx.showHidden && (recurseTimes <= ctx.depth || ctx.depth === null)) { | ||
protoProps = []; | ||
privatePropertyPreviews = getPrivatePropertyPreviews(ctx, value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking individually for privatePropertyPreviews
requires a number of extra checks (everywhere, where protoProps
is also checked). Instead, I suggest to rename protoProps
and to add the private entries there. It is also logical to first show all regular properties, then to show all private members and then at the end all inherited ones.
lib/internal/util/inspect.js
Outdated
} else if (valueType === 'bigint') { | ||
str = stylize(valueDescriptor.unserializableValue, valueType); | ||
} else if (valueType === 'function') { | ||
str = '#[Function] {}'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be useful to know the exact function type and the function name as well:
str = '#[Function] {}'; | |
ctx.indentationLvl += 2; | |
str = `#${formatValue(valueDescriptor.value)}`; | |
ctx.indentationLvl -= 2; |
One possible workaround for core may be to add a different option to |
@joyeecheung adding an option to return a promise in Switching existing APIs to use the inspector would a) likely be slower, b) not work under all circumstances and c) be a breaking change for multiple data types (since we would have to return a promise). |
I'm very glad there might be other possibilities for this. But I found it hard to work out for the following issues: If the private field inspection is going to be asynchronous, it can be not possible of multiple inspection requests at a single run. Since in The two available approaches are
In the asynchronous @joyeecheung please fixes me if I'm not got right. |
Can we just provide an entirely different API, then? It gives you some way to inspect these things in a reliable, well-encapsulated manner, instead of forcing the maintainability burden on either Node.js or V8.
That's why it should be opt-in and not always the default. I think it's reasonable to opt into the cost when you indeed need the underlying data. It is not sustainable to keep making |
I wonder why...can you elaborate on the protocol methods you use in these two approaches? This PR is currently using |
It originally used call frame inspection but I asked that it be changed because enabling the debugger causes v8 to deoptimize things and calling console.log shouldn't make your wasm tier down |
@devsnek I think it’s a separate matter if the user opts into the performance hit of console.log() by setting certain value to true in order to obtain more detailed information. The fundamental issue here is that IMO it’s not realistic to expect that Node.js is able to dig out the guts of objects without the help of deoptimization or any custom inspector - and from what I can tell users should not rely on this feature when performance is critical anyway, and it should be reasonable to keep the inspected result simple when they don’t explicitly opt into detailed results at the cost of perf. |
i don't expect console.log to be fast, but i also know we don't need to tier down to read the private properties of an object. maybe we can convince v8 to provide the debugger apis in a more direct form in the |
@joyeecheung |
I don't think that's necessarily a guarantee that V8 can provide - it could very well refactor the implementation so that it needs to reparse the class body to reflect on this at run time (as that was brought up several times during the implementation of inspector support of private members). Requesting V8 to provide a performant API for this seems to impose unnecessary constraints to them especially when the method belongs to
Ah, I thought #31817 (comment) was about whether it's feasible to do this for an asynchronous API. Then it's not going to be an issue if it's done in an asynchronous manner. |
@joyeecheung i'm find with util.inspect being slow (maybe even involve reparsing, etc), it just shouldn't involve anything that causes the program to remain slow after util.inspect is done. |
@devsnek But the majority of users don't use
|
@joyeecheung all I'm saying is we shouldn't use the |
@devsnek I don't think it's guaranteed by V8 that this shall always be available with just |
I demonstrated a demo of asynchronous API: function previewValueAsync(target) {
return new Promise((resolve, reject) => {
session.once('Debugger.paused', (pausedContext) => {
const callFrames = pausedContext.params.callFrames;
session.post(
'Debugger.evaluateOnCallFrame', {
/** 0. inspector.dispatch, 1. inspector.post, 2. new Promise, 3. previewValueAsync */
callFrameId: callFrames[3].callFrameId,
expression: 'target',
}, (err, result) => {
if (err) {
return reject(err);
}
const { result: evalResult } = result;
if (evalResult.type !== 'object') {
return resolve();
}
if (evalResult.subtype === 'error') {
const error = new Error('Inspection failed: ' + evalResult.description);
return reject(error);
}
session.post('Runtime.getProperties', {
objectId: evalResult.objectId,
}, (err, result) => {
if (err) {
return reject(err);
}
resolve(result);
});
});
});
session.post('Debugger.pause' () => { /* pause doesn't return anything. */ });
return previews;
});
} But I'm wondering that we are still relying on synchronous semantics of |
I think this would not break even if the |
df6fb04
to
2cff044
Compare
According to suggestions made above, I've updated the PR to add a method |
2cff044
to
8965808
Compare
`util.previewValue` may reveal private fields of an object.
8965808
to
c4aca9f
Compare
@legendecas the TODO about inspecting the whole object would likely require to duplicate most of the existing inspect code. Using the separate API is likely not as powerful as I would still love to have a sync API built into |
8ae28ff
to
2935f72
Compare
Ping @legendecas |
According to the current suggestions and ideas, I don't believe the new asynchronous inspection method will be helpful in cases that I was expecting to work out. I'm closing this, anyone who get a use case can pick this up freely, and I'm willing to help too. |
util.previewValue
may reveal private fields of an object.Fixes: #27404
Checklist
Feature list:
reveal properties of nested objects, not supportedCommon list:
make -j4 test
(UNIX), orvcbuild test
(Windows) passes